package com.dbflow5.query;

import com.dbflow5.StringUtils;
import com.dbflow5.annotation.Collate;
import com.dbflow5.config.FlowLog;
import com.dbflow5.config.FlowManager;
import com.dbflow5.converter.TypeConverters;
import com.dbflow5.query.property.TypeConvertedProperty;
import com.dbflow5.sql.Query;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * Description: The class that contains a column name, Operator<T>, and value.
 * This class is mostly reserved for internal use at this point. Using this class directly should be avoided
 * and use the generated [Property] instead.
</T> */
public class Operator<T> extends BaseOperator implements IOperator<T> {

    NameAlias nameAlias;
    private final Class<?> table;
    private final TypeConvertedProperty.TypeConverterGetter getter;
    private final boolean convertToDB;

    private TypeConverters.TypeConverter<?, ?> typeConverter = null;

    public Operator(NameAlias nameAlias, Class<?> table, TypeConvertedProperty.TypeConverterGetter getter, boolean convertToDB){
        super(nameAlias);
        this.nameAlias = nameAlias;
        this.table = table;
        this.getter = getter;
        this.convertToDB = convertToDB;

        if(table != null && getter != null) {
            typeConverter = getter.getTypeConverter(table);
        }
    }

    private boolean convertToString = true;

    @Override
    public String getQuery() {
        return appendToQuery();
    }

    public Operator(Operator<?> operator) {
        this(operator.nameAlias, operator.table, operator.getter, operator.convertToDB);
        this.value = operator.value;
    }

    @Override
    public void appendConditionToQuery(StringBuilder queryBuilder) {
        System.out.println("ROWID:"+operation());
        queryBuilder.append(columnName()).append(operation());

        // Do not use value for certain operators
        // If is raw, we do not want to convert the value to a string.
        if (isValueSet) {
            queryBuilder.append(convertToString? convertObjectToString(value(), true) : value());
        }

        if(postArgument() != null){
            queryBuilder.append(" ");
            queryBuilder.append(postArgument());
        }
    }

    @Override
    public Operator<T> isNull() {
        operation = " "+Operation.IS_NULL+" ";
        return this;
    }

    @Override
    public Operator<T> isNotNull() {
        operation = " "+Operation.IS_NOT_NULL+" ";
        return this;
    }

    @Override
    public Operator<T> is(T value) {
        operation = Operation.EQUALS;
        return value(value);
    }

    @Override
    public Operator<T> eq(T value) {
        return is(value);
    }

    @Override
    public Operator<T> isNot(T value) {
        operation = Operation.NOT_EQUALS;
        return value(value);
    }

    @Override
    public Operator<T> notEq(T value) {
        return isNot(value);
    }

    /**
     * Uses the LIKE operation. Case insensitive comparisons.
     *
     * @param value Uses sqlite LIKE regex to match rows.
     * It must be a string to escape it properly.
     * There are two wildcards: % and _
     * % represents [0,many) numbers or characters.
     * The _ represents a single number or character.
     * @return This condition
     */
    @Override
    public Operator<T> like(String value) {
        operation = " "+Operation.LIKE+" ";
        return value(value);
    }


    /**
     * Uses the MATCH operation. Faster than [like], using an FTS4 Table.
     *
     * . If the WHERE clause of the SELECT statement contains a sub-clause of the form "<column> MATCH ?",
     * FTS is able to use the built-in full-text index to restrict the search to those documents
     * that match the full-text query string specified as the right-hand operand of the MATCH clause.
     *
     * @param value a simple string to match.
     * @return this

     */
    @Override
    public Operator<T> match(String value) {
        operation = " "+Operation.MATCH+" ";
        return value(value);
    }

    /**
     * Uses the NOT LIKE operation. Case insensitive comparisons.
     *
     * @param value Uses sqlite LIKE regex to inversely match rows.
     * It must be a string to escape it properly.
     * There are two wildcards: % and _
     * % represents [0,many) numbers or characters.
     * The _ represents a single number or character.
     * @return this
     *
     */
    @Override
    public Operator<T> notLike(String value) {
        operation = " "+Operation.NOT_LIKE+" ";
        return value(value);
    }

    /**
     * Uses the GLOB operation. Similar to LIKE except it uses case sensitive comparisons.
     *
     * @param value Uses sqlite GLOB regex to match rows.
     * It must be a string to escape it properly.
     * There are two wildcards: * and ?
     * * represents [0,many) numbers or characters.
     * The ? represents a single number or character
     * @return this
     *
     */
    @Override
    public Operator<T> glob(String value) {
        operation = " "+Operation.GLOB+" ";
        return value(value);
    }

    /**
     * The value of the parameter
     *
     * @param value The value of the column in the DB
     * @return this
     *
     */
    public Operator<T> value(Object value) {
        this.value = value;
        isValueSet = true;
        return this;
    }

    @Override
    public Operator<T> greaterThan(T value) {
        operation = Operation.GREATER_THAN;
        return value(value);
    }

    @Override
    public Operator<T> greaterThanOrEq(T value) {
        operation = Operation.GREATER_THAN_OR_EQUALS;
        return value(value);
    }

    @Override
    public Operator<T> lessThan(T value) {
        operation = Operation.LESS_THAN;
        return value(value);
    }

    @Override
    public Operator<T> lessThanOrEq(T value) {
        operation = Operation.LESS_THAN_OR_EQUALS;
        return value(value);
    }

    @Override
    public Operator<T> plus(T value) {
        return assignValueOp(value, Operation.PLUS);
    }

    @Override
    public Operator<T> minus(T value) {
        return assignValueOp(value, Operation.MINUS);
    }

    @Override
    public Operator<T> div(T value) {
        return assignValueOp(value, Operation.DIVISION);
    }

    @Override
    public Operator<T> times(T value) {
        return assignValueOp(value, Operation.MULTIPLY);
    }

    @Override
    public Operator<T> rem(T value) {
        return assignValueOp(value, Operation.MOD);
    }

    /**
     * Add a custom operation to this argument
     *
     * @param operation The SQLite operator
     * @return this
     *
     */
    public Operator<T> operation(String operation) {
        this.operation = operation;
        return this;
    }

    /**
     * Adds a COLLATE to the end of this condition
     *
     * @param collation The SQLite collate function
     * @return this
     * .
     */
    public Operator<T> collate(String collation) {
        postArg = "COLLATE " + collation;
        return this;
    }

    /**
     * Adds a COLLATE to the end of this condition using the [Collate] enum.
     *
     * @param collation The SQLite collate function
     * @return this
     * .
     */
    public Operator<T> collate(Collate collation) {
        if (collation == Collate.NONE) {
            postArg = null;
        } else {
            collate(collation.name());
        }
        return this;
    }

    /**
     * Appends an optional SQL string to the end of this condition
     * @param postfix postfix
     * @return this
     */
    public Operator<T> postfix(String postfix) {
        postArg = postfix;
        return this;
    }

    /**
     * Optional separator when chaining this Operator within a [OperatorGroup]
     *
     * @param separator The separator to use
     * @return This instance
     */
    @Override
    public Operator<T> separator(String separator) {
        this.separator = separator;
        return this;
    }

    @Override
    public Operator<?> is(IConditional conditional) {
        return assignValueOp(conditional, Operation.EQUALS);
    }

    @Override
    public Operator<?> eq(IConditional conditional) {
        return assignValueOp(conditional, Operation.EQUALS);
    }

    @Override
    public Operator<?> isNot(IConditional conditional) {
        return assignValueOp(conditional, Operation.NOT_EQUALS);
    }

    @Override
    public Operator<?> notEq(IConditional conditional) {
        return assignValueOp(conditional, Operation.NOT_EQUALS);
    }

    @Override
    public Operator<T> like(IConditional conditional) {
        return like(conditional.getQuery());
    }

    @Override
    public Operator<T> glob(IConditional conditional) {
        return glob(conditional.getQuery());
    }

    @Override
    public Operator<T> greaterThan(IConditional conditional) {
        return assignValueOp(conditional, Operation.GREATER_THAN);
    }

    @Override
    public Operator<T> greaterThanOrEq(IConditional conditional) {
        return assignValueOp(conditional, Operation.GREATER_THAN_OR_EQUALS);
    }

    @Override
    public Operator<T> lessThan(IConditional conditional) {
        return assignValueOp(conditional, Operation.LESS_THAN);
    }

    @Override
    public Operator<T> lessThanOrEq(IConditional conditional) {
        return assignValueOp(conditional, Operation.LESS_THAN_OR_EQUALS);
    }

    @Override
    public Between<?> between(IConditional conditional) {
        return new Between<>((Operator<Object>)this, conditional);
    }

    @Override
    public In<?> in(IConditional firstConditional, IConditional... conditionals) {
        return new In<>((Operator<Object>)this, firstConditional, true, conditionals);
    }

    @Override
    public In<?> notIn(IConditional firstConditional, IConditional... conditionals) {
        return new In<>((Operator<Object>)this, firstConditional, false, conditionals);
    }

    @Override
    public In<?> notIn(BaseModelQueriable<?> firstBaseModelQueriable, BaseModelQueriable<?>... baseModelQueriables) {
        return new In<>((Operator<Object>)this, firstBaseModelQueriable, false, baseModelQueriables);
    }

    @Override
    public Operator<?> is(BaseModelQueriable<?> baseModelQueriable){
        return assignValueOp(baseModelQueriable, Operation.EQUALS);
    }

    @Override
    public Operator<?> eq(BaseModelQueriable<?> baseModelQueriable) {
        return assignValueOp(baseModelQueriable, Operation.EQUALS);
    }

    @Override
    public Operator<?> isNot(BaseModelQueriable<?> baseModelQueriable)  {
        return assignValueOp(baseModelQueriable, Operation.NOT_EQUALS);
    }

    @Override
    public Operator<?> notEq(BaseModelQueriable<?> baseModelQueriable)  {
        return assignValueOp(baseModelQueriable, Operation.NOT_EQUALS);
    }

    @Override
    public Operator<T> like(BaseModelQueriable<?> baseModelQueriable) {
        return assignValueOp(baseModelQueriable, Operation.LIKE);
    }


    @Override
    public Operator<?> notLike(IConditional conditional) {
        return assignValueOp(conditional, Operation.NOT_LIKE);
    }

     @Override
     public Operator<?> notLike(BaseModelQueriable<?> baseModelQueriable) {
        return assignValueOp(baseModelQueriable, Operation.NOT_LIKE);
    }

    @Override
    public Operator<T> glob(BaseModelQueriable<?> baseModelQueriable) {
        return assignValueOp(baseModelQueriable, Operation.GLOB);
    }    

    @Override
    public Operator<T> greaterThan(BaseModelQueriable<?> baseModelQueriable) {
        return assignValueOp(baseModelQueriable, Operation.GREATER_THAN);
    }    

    @Override
    public Operator<T> greaterThanOrEq( BaseModelQueriable<?> baseModelQueriable ) {
        return assignValueOp(baseModelQueriable, Operation.GREATER_THAN_OR_EQUALS);
    }    

    @Override
    public Operator<T> lessThan( BaseModelQueriable<?> baseModelQueriable ) {
        return assignValueOp(baseModelQueriable, Operation.LESS_THAN);
    }    

    @Override
    public Operator<T> lessThanOrEq( BaseModelQueriable<?> baseModelQueriable ) {
        return assignValueOp(baseModelQueriable, Operation.LESS_THAN_OR_EQUALS);
    }

    public Operator<?> plus(IConditional value) {
        return assignValueOp(value, Operation.PLUS);
    }

    public Operator<?> minus(IConditional value) {
        return assignValueOp(value, Operation.MINUS);
    }

    public Operator<?> div(IConditional value)  {
        return assignValueOp(value, Operation.DIVISION);
    }

    public Operator<?> times(IConditional value)  {
        return assignValueOp(value, Operation.MULTIPLY);
    }

    public Operator<?> rem(IConditional value)  {
        return assignValueOp(value, Operation.MOD);}

    public Operator<?> plus(BaseModelQueriable<?> value) {
        return assignValueOp(value, Operation.PLUS);
    }

    @Override
    public Operator<?> minus(BaseModelQueriable<?> value) {
        return assignValueOp(value, Operation.MINUS);
    }

    @Override
    public Operator<?> div(BaseModelQueriable<?> value) {
        return assignValueOp(value, Operation.DIVISION);
    }

    @Override
    public Operator<?> times(BaseModelQueriable<?> value) {
        return assignValueOp(value, Operation.MULTIPLY);
    }

    @Override
    public Operator<?> rem(BaseModelQueriable<?> value) {
        return assignValueOp(value, Operation.MOD);
    }

    @Override
    public Between<?> between( BaseModelQueriable<?> baseModelQueriable) {
        return new Between<>((Operator<Object>)this, baseModelQueriable);
    }

    @Override
    public In<?> in( BaseModelQueriable<?> firstBaseModelQueriable, BaseModelQueriable<?>... baseModelQueriables) {
        return new In<>((Operator<Object>)this, firstBaseModelQueriable, true, baseModelQueriables);
    }

    @Override
    public Operator<T> concatenate(Object value) {
        Object _value = value;
        operation = Operation.EQUALS + columnName();

        TypeConverters.TypeConverter<?, Object> typeConverter = (TypeConverters.TypeConverter<?, Object>) this.typeConverter;
        if (typeConverter == null && _value != null) {
            typeConverter = (TypeConverters.TypeConverter<?, Object>)FlowManager.getTypeConverterForClass(_value.getClass());
        }
        if (typeConverter != null && convertToDB) {
            _value = typeConverter.getDBValue(_value);
        }

        if(_value instanceof String || _value instanceof IOperator<?> || _value instanceof  Character){
            operation = operation+" "+Operation.CONCATENATE+" ";
        }else if(_value instanceof Number){
            operation = operation+" "+Operation.PLUS+" ";
        }else {
            assert _value != null;
            throw new IllegalArgumentException(
                    String.valueOf(_value.getClass()));
        }

        this.value = _value;
        isValueSet = true;
        return this;
    }

    @Override
    public Operator<T> concatenate(IConditional conditional) {
        return concatenate((Object) conditional);
    }

    /**
     * Turns this condition into a SQL BETWEEN operation
     *
     * @param value The value of the first argument of the BETWEEN clause
     * @return Between operator
     */
    @Override
    public Between<T> between(T value) {
        return new Between<>(this, value);
    }

    @SafeVarargs
    @Override
    public final In<T> in(T firstValue, T... values) {
        return new In<>(this, firstValue, true, values);
    }

    @SafeVarargs
    @Override
    public final In<T> notIn(T firstValue, T... values) {
        return new In<>(this, firstValue, false, values);
    }

    @Override
    public In<T> in(Collection<T> values) {
        return new In<>(this, values, true);
    }

    @Override
    public In<T> notIn(Collection<T> values) {
        return new In<>(this, values, false);
    }

    @Override
    public String convertObjectToString(Object obj, boolean appendInnerParenthesis) {
        TypeConverters.TypeConverter<?, Object> newTypeConverter = (TypeConverters.TypeConverter<?, Object>)typeConverter;
        if(newTypeConverter != null){
            Object converted = obj;
            try {
                converted = convertToDB? newTypeConverter.getDBValue(obj) : obj;
            } catch (ClassCastException c) {
                // if object type is not valid converted type, just use type as is here.
                // if object type is not valid converted type, just use type as is here.
                FlowLog.log(FlowLog.Level.I, "Value passed to operation is not valid type" +
                        " for TypeConverter in the column. Preserving value $obj to be used as is.");
            }

            return convertValueToString(converted, appendInnerParenthesis, false);
        }else {
            return super.convertObjectToString(obj, appendInnerParenthesis);
        }
    }

    private Operator<T> assignValueOp(Object value, String operation) {
        if (!isValueSet) {
            this.operation = operation;
            return value(value);
        } else {
            convertToString = false;
            // convert value to a string value because of conversion.
            return value(convertValueToString(this.value) + operation + convertValueToString(value));
        }
    }

    /**
     * Static constants that define condition operations
     */
    public static class Operation {

        /**
         * Equals comparison
         */
        public static final String EQUALS = "=";

        /**
         * Not-equals comparison
         */
        public static final String NOT_EQUALS = "!=";

        /**
         * String concatenation
         */
        public static final String CONCATENATE = "||";

        /**
         * Number addition
         */
        public static final String PLUS = "+";

        /**
         * Number subtraction
         */
        public static final String MINUS = "-";

        public static final String DIVISION = "/";

        public static final String MULTIPLY = "*";

        public static final String MOD = "%";

        /**
         * If something is LIKE another (a case insensitive search).
         * There are two wildcards: % and _
         * % represents [0,many) numbers or characters.
         * The _ represents a single number or character.
         */
        public static final String LIKE = "LIKE";

        /**
         * If the WHERE clause of the SELECT statement contains a sub-clause of the form "<column> MATCH ?",
         * FTS is able to use the built-in full-text index to restrict the search to those documents
         * that match the full-text query string specified as the right-hand operand of the MATCH clause.
         */
        public static final String MATCH = "MATCH";

        /**
         * If something is NOT LIKE another (a case insensitive search).
         * There are two wildcards: % and _
         * % represents [0,many) numbers or characters.
         * The _ represents a single number or character.
         */
        public static final String NOT_LIKE = "NOT LIKE";

        /**
         * If something is case sensitive like another.
         * It must be a string to escape it properly.
         * There are two wildcards: * and ?
         * * represents [0,many) numbers or characters.
         * The ? represents a single number or character
         */
        public static final String GLOB = "GLOB";

        /**
         * Greater than some value comparison
         */
        public static final String GREATER_THAN = ">";

        /**
         * Greater than or equals to some value comparison
         */
        public static final String GREATER_THAN_OR_EQUALS = ">=";

        /**
         * Less than some value comparison
         */
        public static final String LESS_THAN = "<";

        /**
         * Less than or equals to some value comparison
         */
        public static final String LESS_THAN_OR_EQUALS = "<=";

        /**
         * Between comparison. A simplification of X&lt;Y AND Y&lt;Z to Y BETWEEN X AND Z
         */
        public static final String BETWEEN = "BETWEEN";

        /**
         * AND comparison separator
         */
        public static final String AND = "AND";

        /**
         * OR comparison separator
         */
        public static final String OR = "OR";

        /**
         * An empty value for the condition.
         */
        @Deprecated
        public static final String EMPTY_PARAM = "?";

        /**
         * Special operation that specify if the column is not null for a specified row. Use of this as
         * an operator will ignore the value of the [Operator] for it.
         */
        public static final String IS_NOT_NULL = "IS NOT NULL";

        /**
         * Special operation that specify if the column is null for a specified row. Use of this as
         * an operator will ignore the value of the [Operator] for it.
         */
        public static final String IS_NULL = "IS NULL";

        /**
         * The SQLite IN command that will select rows that are contained in a list of values.
         * EX: SELECT * from Table where column IN ('first', 'second', etc)
         */
        public static final String IN = "IN";

        /**
         * The reverse of the [.IN] command that selects rows that are not contained
         * in a list of values specified.
         */
        public static final String NOT_IN = "NOT IN";
    }

    /**
     * The SQL BETWEEN operator that contains two values instead of the normal 1.
     */
    public static class Between<V> extends BaseOperator implements Query {

        private V secondValue = null;

        Between(Operator<V> operator, V value) {
            super(operator.nameAlias);
            this.operation = " "+Operation.BETWEEN+" ";
            this.value = value;
            isValueSet = true;
            this.postArg = operator.postArgument();
        }

        @Override
        public String getQuery() {
            return appendToQuery();
        }

        public Between<V> and(V secondValue) {
            this.secondValue = secondValue;
            return this;
        }

        public V secondValue() {
            return secondValue;
        }

        @Override
        public void appendConditionToQuery(StringBuilder queryBuilder) {
            queryBuilder.append(columnName()).append(operation())
                .append(convertObjectToString(value(), true))
                .append(" "+Operation.AND+" ")
                .append(convertObjectToString(secondValue(), true))
                .append(" ");

            StringUtils.appendOptional(queryBuilder, postArgument());
        }
    }

    /**
     * The SQL IN and NOT IN operator that specifies a list of values to SELECT rows from.
     * EX: SELECT * FROM myTable WHERE columnName IN ('column1','column2','etc')
     */
    public static class In<V> extends BaseOperator implements Query {

        private final List<V> inArguments = new ArrayList<>();

        @Override
        public String getQuery() {
            return appendToQuery();
        }

        /**
         * Creates a new instance
         *
         * @param operator      The operator object to pass in. We only use the column name here.
         * @param firstArgument The first value in the IN query as one is required.
         * @param isIn          if this is an [Operator.Operation.IN]
         * statement or a [Operator.Operation.NOT_IN]
         */
        @SafeVarargs
        In(Operator<V> operator, V firstArgument, boolean isIn, V... arguments) {
            super(operator.columnAlias());
            inArguments.add(firstArgument);
            inArguments.addAll(Arrays.asList(arguments));
            if(isIn){
                operation = Operation.IN;
            }else {
                operation = Operation.NOT_IN;
            }
        }

        In(Operator<V> operator, Collection<V> args, boolean isIn) {
            super(operator.columnAlias());
            inArguments.addAll(args);
            if(isIn){
                operation = Operation.IN;
            }else {
                operation = Operation.NOT_IN;
            }
        }

        /**
         * Appends another value to this In statement
         *
         * @param argument The non-type converted value of the object. The value will be converted
         * in a [OperatorGroup].
         * @return In<V>
         */
        public In<V> and(V argument) {
            inArguments.add(argument);
            return this;
        }

        @Override
        public void appendConditionToQuery(StringBuilder queryBuilder) {
            queryBuilder.append(columnName())
                .append(" "+operation()+" ")
                .append("(")
                .append(joinArguments(",", inArguments, this))
                .append(")");
        }
    }

    public static String convertValueToString(Object value) {
        return convertValueToString(value, false);
    }

    public static <T> Operator<T> op(NameAlias column) {
        return new Operator<>(column, null, null, false);
    }

    public static <T> Operator<T> op(NameAlias alias, Class<?> table, TypeConvertedProperty.TypeConverterGetter getter, boolean convertToDB) {
        return new Operator<>(alias, table, getter, convertToDB);
    }

    public static <T> Operator<T> op(String column) {
        return NameAlias.nameAlias(column).op();
    }

    public OperatorGroup and(SQLOperator sqlOperator) {
        return OperatorGroup.clause(this).and(sqlOperator);
    }

    public OperatorGroup or(SQLOperator sqlOperator) {
        return OperatorGroup.clause(this).or(sqlOperator);
    }

    public OperatorGroup andAll(Collection<SQLOperator> sqlOperator) {
        return OperatorGroup.clause(this).andAll(sqlOperator);
    }

    public OperatorGroup orAll(Collection<SQLOperator> sqlOperator) {
        return OperatorGroup.clause(this).orAll(sqlOperator);
    }

    public Operator.In<T> in(T[] values) {
        if(values.length == 1){
            return in(values[0]);
        }else {
            return this.in(values[0], Arrays.copyOfRange(values, 1, values.length));
        }
    }

    public Operator.In<T> notIn(T[] values) {
        if(values.length == 1){
            return notIn(values[0]);
        }else {
            return this.notIn(values[0], Arrays.copyOfRange(values, 1, values.length));
        }
    }

}
